/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tencent.kuikly.compose.foundation.pager

import androidx.annotation.FloatRange
import androidx.annotation.IntRange as AndroidXIntRange
import com.tencent.kuikly.compose.animation.core.AnimationSpec
import com.tencent.kuikly.compose.animation.core.animate
import com.tencent.kuikly.compose.animation.core.spring
import com.tencent.kuikly.compose.foundation.ExperimentalFoundationApi
import com.tencent.kuikly.compose.foundation.MutatePriority
import com.tencent.kuikly.compose.foundation.gestures.Orientation
import com.tencent.kuikly.compose.foundation.gestures.ScrollScope
import com.tencent.kuikly.compose.foundation.gestures.ScrollableState
import com.tencent.kuikly.compose.foundation.gestures.stopScroll
import com.tencent.kuikly.compose.foundation.interaction.InteractionSource
import com.tencent.kuikly.compose.foundation.interaction.MutableInteractionSource
import com.tencent.kuikly.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
import com.tencent.kuikly.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
import com.tencent.kuikly.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import com.tencent.kuikly.compose.foundation.lazy.layout.ObservableScopeInvalidator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.structuralEqualityPolicy
import com.tencent.kuikly.compose.foundation.gestures.snapping.SnapPosition
import com.tencent.kuikly.compose.foundation.layout.PaddingValues
import com.tencent.kuikly.compose.ui.geometry.Offset
import com.tencent.kuikly.compose.ui.layout.AlignmentLine
import com.tencent.kuikly.compose.ui.layout.MeasureResult
import com.tencent.kuikly.compose.ui.layout.Remeasurement
import com.tencent.kuikly.compose.ui.layout.RemeasurementModifier
import com.tencent.kuikly.compose.ui.unit.Constraints
import com.tencent.kuikly.compose.ui.unit.Density
import com.tencent.kuikly.compose.ui.unit.dp
import com.tencent.kuikly.compose.scroller.applyScrollViewOffsetDelta
import com.tencent.kuikly.compose.scroller.kuiklyInfo
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.roundToLong
import kotlin.math.sign
import kotlin.ranges.IntRange
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.max

/**
 * Creates and remember a [PagerState] to be used with a [Pager]
 *
 * Please refer to the sample to learn how to use this API.
 * @sample androidx.compose.foundation.samples.PagerWithStateSample
 *
 * @param initialPage The pager that should be shown first.
 * @param initialPageOffsetFraction The offset of the initial page as a fraction of the page size.
 * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the
 * snapped position.
 * @param pageCount The amount of pages this Pager will have.
 */
@Composable
fun rememberPagerState(
    initialPage: Int = 0,
    @FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f,
    pageCount: () -> Int
): PagerState {
    return rememberSaveable(saver = DefaultPagerState.Saver) {
        DefaultPagerState(
            initialPage,
            initialPageOffsetFraction,
            pageCount
        )
    }.apply {
        pageCountState.value = pageCount
    }
}

/**
 * Creates a default [PagerState] to be used with a [Pager]
 *
 * Please refer to the sample to learn how to use this API.
 * @sample androidx.compose.foundation.samples.PagerWithStateSample
 *
 * @param currentPage The pager that should be shown first.
 * @param currentPageOffsetFraction The offset of the initial page as a fraction of the page size.
 * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the
 * snapped position.
 * @param pageCount The amount of pages this Pager will have.
 */
fun PagerState(
    currentPage: Int = 0,
    @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
    pageCount: () -> Int
): PagerState = DefaultPagerState(currentPage, currentPageOffsetFraction, pageCount)

private class DefaultPagerState(
    currentPage: Int,
    currentPageOffsetFraction: Float,
    updatedPageCount: () -> Int
) : PagerState(currentPage, currentPageOffsetFraction) {

    var pageCountState = mutableStateOf(updatedPageCount)
    override val pageCount: Int get() = pageCountState.value.invoke()

    companion object {
        /**
         * To keep current page and current page offset saved
         */
        val Saver: Saver<DefaultPagerState, *> = listSaver(
            save = {
                listOf(
                    it.currentPage,
                    (it.currentPageOffsetFraction).coerceIn(MinPageOffset, MaxPageOffset),
                    it.pageCount
                )
            },
            restore = {
                DefaultPagerState(
                    currentPage = it[0] as Int,
                    currentPageOffsetFraction = it[1] as Float,
                    updatedPageCount = { it[2] as Int }
                )
            }
        )
    }
}

/**
 * The state that can be used to control [VerticalPager] and [HorizontalPager]
 */
@OptIn(ExperimentalFoundationApi::class)
@Stable
abstract class PagerState internal constructor(
    currentPage: Int = 0,
    @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
//    prefetchScheduler: PrefetchScheduler? = null
) : ScrollableState {

//    /**
//     * @param currentPage The initial page to be displayed
//     * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
//     * the layout.
//     */
//    constructor(
//        currentPage: Int = 0,
//        @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f
//    ) : this(currentPage, currentPageOffsetFraction, null)

    /**
     * The total amount of pages present in this pager. The source of this data should be
     * observable.
     */
    abstract val pageCount: Int

    init {
        require(currentPageOffsetFraction in -0.5..0.5) {
            "currentPageOffsetFraction $currentPageOffsetFraction is " +
                "not within the range -0.5 to 0.5"
        }
    }

    override var contentPadding: PaddingValues = PaddingValues(0.dp)

    /**
     * Difference between the last up and last down events of a scroll event.
     */
    internal var upDownDifference: Offset by mutableStateOf(Offset.Zero)
    private val animatedScrollScope = PagerLazyAnimateScrollScope(this)

    private val scrollPosition = PagerScrollPosition(currentPage, currentPageOffsetFraction, this)

    internal var firstVisiblePage = currentPage
        private set

    internal var firstVisiblePageOffset = 0
        private set

    private var maxScrollOffset: Long = Long.MAX_VALUE

    private var minScrollOffset: Long = 0L

    private var accumulator: Float = 0.0f

    /**
     * The prefetch will act after the measure pass has finished and it needs to know the
     * magnitude and direction of the scroll that triggered the measure pass
     */
    private var previousPassDelta = 0f

    /**
     * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
     * we reached the end of the list.
     */
    internal val scrollableState = ScrollableState { performScroll(it) }

    /**
     * Within the scrolling context we can use absolute positions to
     * determine scroll deltas and max min scrolling.
     */
    private fun performScroll(delta: Float): Float {
        val currentScrollPosition = currentAbsoluteScrollOffset()
        debugLog {
            "\nDelta=$delta " +
                "\ncurrentScrollPosition=$currentScrollPosition " +
                "\naccumulator=$accumulator" +
                "\nmaxScrollOffset=$maxScrollOffset"
        }

        val decimalAccumulation = (delta + accumulator)
        val decimalAccumulationInt = decimalAccumulation.roundToLong()
        accumulator = decimalAccumulation - decimalAccumulationInt

        // nothing to scroll
        if (delta.absoluteValue < 1e-4f) return delta

        /**
         * The updated scroll position is the current position with the integer part of the delta
         * and accumulator applied.
         */
        val updatedScrollPosition = (currentScrollPosition + decimalAccumulationInt)

        /**
         * Check if the scroll position may be larger than the maximum possible scroll.
         */
        val coercedScrollPosition = updatedScrollPosition.coerceIn(minScrollOffset, maxScrollOffset)

        /**
         * Check if we actually coerced.
         */
        val changed = updatedScrollPosition != coercedScrollPosition

        /**
         * Calculated the actual scroll delta to be applied
         */
        val scrollDelta = coercedScrollPosition - currentScrollPosition

        previousPassDelta = scrollDelta.toFloat()

        if (scrollDelta.absoluteValue != 0L) {
            isLastScrollForwardState.value = scrollDelta > 0.0f
            isLastScrollBackwardState.value = scrollDelta < 0.0f
        }

        /**
         * Apply the scroll delta
         */
        val layoutInfo = pagerLayoutInfoState.value
        if (layoutInfo.tryToApplyScrollWithoutRemeasure(-scrollDelta.toInt())) {
            debugLog { "Will Apply Without Remeasure" }
            applyMeasureResult(
                result = layoutInfo,
                visibleItemsStayedTheSame = true
            )
            // we don't need to remeasure, so we only trigger re-placement:
            placementScopeInvalidator.invalidateScope()
            layoutWithoutMeasurement++
        } else {
            debugLog { "Will Apply With Remeasure" }
            scrollPosition.applyScrollDelta(scrollDelta.toInt())
            remeasurement?.forceRemeasure()
            layoutWithMeasurement++
        }

        // Return the consumed value.
        return (if (changed) scrollDelta else delta).toFloat()
    }

    /**
     * Only used for testing to confirm that we're not making too many measure passes
     */
    internal val numMeasurePasses: Int get() = layoutWithMeasurement + layoutWithoutMeasurement

    internal var layoutWithMeasurement: Int = 0
        private set

    private var layoutWithoutMeasurement: Int = 0

    /**
     * Only used for testing to disable prefetching when needed to test the main logic.
     */
    internal var prefetchingEnabled: Boolean = true

    /**
     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
     */
    private var indexToPrefetch = -1

//    /**
//     * The handle associated with the current index from [indexToPrefetch].
//     */
//    private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null

    /**
     * Keeps the scrolling direction during the previous calculation in order to be able to
     * detect the scrolling direction change.
     */
    private var wasPrefetchingForward = false

    /** Backing state for PagerLayoutInfo */
    private var pagerLayoutInfoState =
        mutableStateOf(EmptyLayoutInfo, neverEqualPolicy())

    /**
     * A [PagerLayoutInfo] that contains useful information about the Pager's last layout pass.
     * For instance, you can query which pages are currently visible in the layout.
     *
     * This property is observable and is updated after every scroll or remeasure.
     * If you use it in the composable function it will be recomposed on every change causing
     * potential performance issues including infinity recomposition loop.
     * Therefore, avoid using it in the composition.
     *
     * If you want to run some side effects like sending an analytics event or updating a state
     * based on this value consider using "snapshotFlow":
     * @sample androidx.compose.foundation.samples.UsingPagerLayoutInfoForSideEffectSample
     */
    val layoutInfo: PagerLayoutInfo get() = pagerLayoutInfoState.value

    internal val pageSpacing: Int
        get() = pagerLayoutInfoState.value.pageSpacing

    internal val pageSize: Int
        get() = pagerLayoutInfoState.value.pageSize

    internal var density: Density = UnitDensity

    internal val pageSizeWithSpacing: Int
        get() = pageSize + pageSpacing

    /**
     * How far the current page needs to scroll so the target page is considered to be the next
     * page.
     */
    internal val positionThresholdFraction: Float
        get() = with(density) {
            val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
            minThreshold / pageSize.toFloat()
        }

    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()

    /**
     * [InteractionSource] that will be used to dispatch drag events when this
     * list is being dragged. If you want to know whether the fling (or animated scroll) is in
     * progress, use [isScrollInProgress].
     */
    val interactionSource: InteractionSource
        get() = internalInteractionSource

    /**
     * The page that sits closest to the snapped position. This is an observable value and will
     * change as the pager scrolls either by gesture or animation.
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
     *
     */
    val currentPage: Int get() = scrollPosition.currentPage

    private var programmaticScrollTargetPage by mutableIntStateOf(-1)

    private var settledPageState by mutableIntStateOf(currentPage)

    /**
     * The page that is currently "settled". This is an animation/gesture unaware page in the sense
     * that it will not be updated while the pages are being scrolled, but rather when the
     * animation/scroll settles.
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
     */
    val settledPage by derivedStateOf(structuralEqualityPolicy()) {
        if (isScrollInProgress) {
            settledPageState
        } else {
            this.currentPage
        }
    }

    /**
     * The page this [Pager] intends to settle to.
     * During fling or animated scroll (from [animateScrollToPage] this will represent the page
     * this pager intends to settle to. When no scroll is ongoing, this will be equal to
     * [currentPage].
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
     */
    val targetPage: Int by derivedStateOf(structuralEqualityPolicy()) {
        val finalPage = if (!isScrollInProgress) {
            this.currentPage
        } else if (programmaticScrollTargetPage != -1) {
            programmaticScrollTargetPage
        } else {
            // act on scroll only
            if (abs(this.currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
                if (lastScrolledForward) {
                    firstVisiblePage + 1
                } else {
                    firstVisiblePage
                }
            } else {
                this.currentPage
            }
        }
        finalPage.coerceInPageRange()
    }

    /**
     * Indicates how far the current page is to the snapped position, this will vary from
     * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
     * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
     * flip once the current page changes.
     *
     * This property is observable and shouldn't be used as is in a composable function due to
     * potential performance issues. To use it in the composition, please consider using a
     * derived state (e.g [derivedStateOf]) to only have recompositions when the derived
     * value changes.
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
     */
    val currentPageOffsetFraction: Float get() = scrollPosition.currentPageOffsetFraction

//    internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler)

    internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()

    /**
     * Provides a modifier which allows to delay some interactions (e.g. scroll)
     * until layout is ready.
     */
    internal val awaitLayoutModifier = AwaitFirstLayoutModifier()

    /**
     * The [Remeasurement] object associated with our layout. It allows us to remeasure
     * synchronously during scroll.
     */
    internal var remeasurement: Remeasurement? by mutableStateOf(null)
        private set

    /**
     * The modifier which provides [remeasurement].
     */
    internal val remeasurementModifier = object : RemeasurementModifier {
        override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
            this@PagerState.remeasurement = remeasurement
        }
    }

    /**
     * Constraints passed to the prefetcher for premeasuring the prefetched items.
     */
    internal var premeasureConstraints = Constraints()

    /**
     * Stores currently pinned pages which are always composed, used by for beyond bound pages.
     */
    internal val pinnedPages = LazyLayoutPinnedItemList()

    internal val nearestRange: IntRange by scrollPosition.nearestRangeState

    internal val placementScopeInvalidator = ObservableScopeInvalidator()

    /**
     * Scroll (jump immediately) to a given [page].
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.ScrollToPageSample
     *
     * @param page The destination page to scroll to
     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
     * destination page will be offset from its snapped position.
     */
    suspend fun scrollToPage(
        page: Int,
        @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0f
    ) = scroll {
        debugLog { "Scroll from page=$currentPage to page=$page" }
        awaitScrollDependencies()
        require(pageOffsetFraction in -0.5..0.5) {
            "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
        }
        val targetPage = page.coerceInPageRange()
        snapToItem(targetPage, pageOffsetFraction, forceRemeasure = true)
    }

//    /**
//     * Jump immediately to a given [page] with a given [pageOffsetFraction] inside
//     * a [ScrollScope]. Use this method to create custom animated scrolling experiences. This will
//     * update the value of [currentPage] and [currentPageOffsetFraction] immediately, but can only
//     * be used inside a [ScrollScope], use [scroll] to gain access to a [ScrollScope].
//     *
//     * Please refer to the sample to learn how to use this API.
//     * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
//     *
//     * @param page The destination page to scroll to
//     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
//     * destination page will be offset from its snapped position.
//     */
//    @ExperimentalFoundationApi
//    fun ScrollScope.updateCurrentPage(
//        page: Int,
//        @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f
//    ) {
//        snapToItem(page, pageOffsetFraction, forceRemeasure = true)
//    }

//    /**
//     * Used to update [targetPage] during a programmatic scroll operation. This can only be called
//     * inside a [ScrollScope] and should be called anytime a custom scroll (through [scroll]) is
//     * executed in order to correctly update [targetPage]. This will not move the pages and it's
//     * still the responsibility of the caller to call [ScrollScope.scrollBy] in order to actually
//     * get to [targetPage]. By the end of the [scroll] block, when the [Pager] is no longer
//     * scrolling [targetPage] will assume the value of [currentPage].
//     *
//     * Please refer to the sample to learn how to use this API.
//     * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
//     */
//    @ExperimentalFoundationApi
//    fun ScrollScope.updateTargetPage(targetPage: Int) {
//        programmaticScrollTargetPage = targetPage.coerceInPageRange()
//    }

    internal fun snapToItem(page: Int, offsetFraction: Float, forceRemeasure: Boolean) {
        val distance = animatedScrollScope.calculateDistanceTo(page) + offsetFraction * pageSizeWithSpacing
        dispatchRawDelta(distance)
    }

    internal val measurementScopeInvalidator = ObservableScopeInvalidator()

    /**
     * Requests the [page] to be at the snapped position during the next remeasure,
     * offset by [pageOffsetFraction], and schedules a remeasure.
     *
     * The scroll position will be updated to the requested position rather than maintain
     * the index based on the current page key (when a data set change will also be
     * applied during the next remeasure), but *only* for the next remeasure.
     *
     * Any scroll in progress will be cancelled.
     *
     * @param page the index to which to scroll. Must be non-negative.
     * @param pageOffsetFraction the offset fraction that the page should end up after the scroll.
     */
    fun requestScrollToPage(
        @AndroidXIntRange(from = 0) page: Int,
        @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f
    ) {
        // Cancel any scroll in progress.
        if (isScrollInProgress) {
            pagerLayoutInfoState.value.coroutineScope.launch {
                stopScroll()
            }
        }
        snapToItem(page.coerceInPageRange(), pageOffsetFraction, forceRemeasure = false)
    }

    /**
     * Scroll animate to a given [page]. If the [page] is too far away from [currentPage] we will
     * not compose all pages in the way. We will pre-jump to a nearer page, compose and animate
     * the rest of the pages until [page].
     *
     * Please refer to the sample to learn how to use this API.
     * @sample androidx.compose.foundation.samples.AnimateScrollPageSample
     *
     * @param page The destination page to scroll to
     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
     * destination page will be offset from its snapped position.
     * @param animationSpec An [AnimationSpec] to move between pages. We'll use a [spring] as the
     * default animation.
     */
    suspend fun animateScrollToPage(
        page: Int,
        @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0f,
        animationSpec: AnimationSpec<Float> = spring()
    ) {
        if (page == currentPage && currentPageOffsetFraction == pageOffsetFraction ||
            pageCount == 0
        ) return
        awaitScrollDependencies()
        require(pageOffsetFraction in -0.5..0.5) {
            "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
        }
        val targetPage = page.coerceInPageRange()
        val targetPageOffsetToSnappedPosition =
            (pageOffsetFraction * pageSizeWithSpacing)
        val distance: Float = animatedScrollScope.calculateDistanceTo(targetPage) + targetPageOffsetToSnappedPosition
        var targetOffset = distance + kuiklyInfo.contentOffset
        if (targetOffset < 0) {
            targetOffset = minScrollOffset.toFloat()
        } else if (targetOffset + kuiklyInfo.viewportSize > kuiklyInfo.currentContentSize) {
            targetOffset = maxScrollOffset.toFloat()
        }
        kuiklyInfo.run {
            val targetOffsetDp = if (isVertical()) {
                Offset(scrollView?.curOffsetX ?: 0f, targetOffset / getDensity() - 0.01f)
            } else {
                Offset(targetOffset / getDensity() - 0.01f, scrollView?.curOffsetY ?: 0f)
            }
            scrollView?.setContentOffset(targetOffsetDp.x, targetOffsetDp.y, true)
        }
//
//        animatedScrollScope.animateScrollToPage(
//            targetPage,
//            targetPageOffsetToSnappedPosition,
//            animationSpec,
//            updateTargetPage = { updateTargetPage(it) }
//        )
    }

    private suspend fun awaitScrollDependencies() {
        awaitLayoutModifier.waitForFirstLayout()
    }

    override suspend fun scroll(
        scrollPriority: MutatePriority,
        block: suspend ScrollScope.() -> Unit
    ) {
        awaitScrollDependencies()
        // will scroll and it's not scrolling already update settled page
        if (!isScrollInProgress) {
            settledPageState = currentPage
        }
        scrollableState.scroll(scrollPriority, block)
        programmaticScrollTargetPage = -1 // reset animated scroll target page indicator
    }

    override fun dispatchRawDelta(delta: Float): Float {
        return scrollableState.dispatchRawDelta(delta)
    }

    override val isScrollInProgress: Boolean
        get() = scrollableState.isScrollInProgress

    final override var canScrollForward: Boolean by mutableStateOf(false)
        private set
    final override var canScrollBackward: Boolean by mutableStateOf(false)
        private set

    private val isLastScrollForwardState = mutableStateOf(false)
    private val isLastScrollBackwardState = mutableStateOf(false)

    @get:Suppress("GetterSetterNames")
    override val lastScrolledForward: Boolean
        get() = isLastScrollForwardState.value

    @get:Suppress("GetterSetterNames")
    override val lastScrolledBackward: Boolean
        get() = isLastScrollBackwardState.value

    /**
     *  Updates the state with the new calculated scroll position and consumed scroll.
     */
    internal fun applyMeasureResult(
        result: PagerMeasureResult,
        visibleItemsStayedTheSame: Boolean = false
    ) {
        debugLog { "Applying Measure Result" }
        if (visibleItemsStayedTheSame) {
            scrollPosition.updateCurrentPageOffsetFraction(result.currentPageOffsetFraction)
        } else {
            scrollPosition.updateFromMeasureResult(result)
            cancelPrefetchIfVisibleItemsChanged(result)
        }
        pagerLayoutInfoState.value = result
        canScrollForward = result.canScrollForward
        canScrollBackward = result.canScrollBackward
        result.firstVisiblePage?.let { firstVisiblePage = it.index }
        firstVisiblePageOffset = result.firstVisiblePageScrollOffset
        tryRunPrefetch(result)
        maxScrollOffset = result.calculateNewMaxScrollOffset(pageCount)
        minScrollOffset = result.calculateNewMinScrollOffset(pageCount)
        debugLog {
            "Finished Applying Measure Result" +
                "\nNew maxScrollOffset=$maxScrollOffset"
        }

        val composeOffset = currentAbsoluteScrollOffset()
        val layoutSize = if (result.orientation == Orientation.Horizontal)
            result.viewportSize.width else result.viewportSize.height

        // 非滚动状态，当前的offset和scrolview的不对齐； 调整下；
        if (!isScrollInProgress && scrollableState.kuiklyInfo.contentOffset < composeOffset.toInt()) {
            kuiklyInfo.run {
                appleScrollViewOffsetJob?.cancel()
                appleScrollViewOffsetJob = scope?.launch {
                    delay(50)
                    // 先扩容
                    currentContentSize = max((maxScrollOffset + layoutSize).toInt(), currentContentSize)
                    updateContentSizeToRender()

                    val delta = composeOffset.toInt() - scrollableState.kuiklyInfo.contentOffset

                    // 调整pager的offset
                    applyScrollViewOffsetDelta(delta)
                }
            }
        }
    }

    private fun tryRunPrefetch(result: PagerMeasureResult) = Snapshot.withoutReadObservation {
        if (abs(previousPassDelta) > 0.5f) {
            if (prefetchingEnabled && isGestureActionMatchesScroll(previousPassDelta)) {
                notifyPrefetch(previousPassDelta, result)
            }
        }
    }

    private fun Int.coerceInPageRange() = if (pageCount > 0) {
        coerceIn(0, pageCount - 1)
    } else {
        0
    }

    // check if the scrolling will be a result of a fling operation. That is, if the scrolling
    // direction is in the opposite direction of the gesture movement. Also, return true if there
    // is no applied gesture that causes the scrolling
    private fun isGestureActionMatchesScroll(scrollDelta: Float): Boolean =
        if (layoutInfo.orientation == Orientation.Vertical) {
            sign(scrollDelta) == sign(-upDownDifference.y)
        } else {
            sign(scrollDelta) == sign(-upDownDifference.x)
        } || isNotGestureAction()

    internal fun isNotGestureAction(): Boolean =
        upDownDifference.x.toInt() == 0 && upDownDifference.y.toInt() == 0

    private fun notifyPrefetch(delta: Float, info: PagerLayoutInfo) {
        if (!prefetchingEnabled) {
            return
        }

        if (info.visiblePagesInfo.isNotEmpty()) {
            val isPrefetchingForward = delta > 0
            val indexToPrefetch = if (isPrefetchingForward) {
                info.visiblePagesInfo.last().index + info.beyondViewportPageCount + PagesToPrefetch
            } else {
                info.visiblePagesInfo.first().index - info.beyondViewportPageCount - PagesToPrefetch
            }
            if (indexToPrefetch in 0 until pageCount) {
                if (indexToPrefetch != this.indexToPrefetch) {
                    if (wasPrefetchingForward != isPrefetchingForward) {
                        // the scrolling direction has been changed which means the last prefetched
                        // is not going to be reached anytime soon so it is safer to dispose it.
                        // if this item is already visible it is safe to call the method anyway
                        // as it will be no-op
//                        currentPrefetchHandle?.cancel()
                    }
                    this.wasPrefetchingForward = isPrefetchingForward
                    this.indexToPrefetch = indexToPrefetch
//                    currentPrefetchHandle = prefetchState.schedulePrefetch(
//                        indexToPrefetch, premeasureConstraints
//                    )
                }
                if (isPrefetchingForward) {
                    val lastItem = info.visiblePagesInfo.last()
                    val pageSize = info.pageSize + info.pageSpacing
                    val distanceToReachNextItem =
                        lastItem.offset + pageSize - info.viewportEndOffset
                    // if in the next frame we will get the same delta will we reach the item?
                    if (distanceToReachNextItem < delta) {
//                        currentPrefetchHandle?.markAsUrgent()
                    }
                } else {
                    val firstItem = info.visiblePagesInfo.first()
                    val distanceToReachNextItem = info.viewportStartOffset - firstItem.offset
                    // if in the next frame we will get the same delta will we reach the item?
                    if (distanceToReachNextItem < -delta) {
//                        currentPrefetchHandle?.markAsUrgent()
                    }
                }
            }
        }
    }

    private fun cancelPrefetchIfVisibleItemsChanged(info: PagerLayoutInfo) {
        if (indexToPrefetch != -1 && info.visiblePagesInfo.isNotEmpty()) {
            val expectedPrefetchIndex = if (wasPrefetchingForward) {
                info.visiblePagesInfo.last().index + info.beyondViewportPageCount + PagesToPrefetch
            } else {
                info.visiblePagesInfo.first().index - info.beyondViewportPageCount - PagesToPrefetch
            }
            if (indexToPrefetch != expectedPrefetchIndex) {
                indexToPrefetch = -1
//                currentPrefetchHandle?.cancel()
//                currentPrefetchHandle = null
            }
        }
    }

    /**
     * An utility function to help to calculate a given page's offset. This is an offset that
     * represents how far [page] is from the settled position (represented by [currentPage]
     * offset). The difference here is that [currentPageOffsetFraction] is a value between -0.5 and
     * 0.5 and the value calculated by this function can be larger than these numbers if [page] is
     * different than [currentPage].
     *
     * For instance, if currentPage=0 and we call [getOffsetDistanceInPages] for page 3, the result
     * will be 3, meaning the given page is 3 pages away from the current page (the sign represent
     * the direction of the offset, positive is forward, negative is backwards). Another example is
     * if currentPage=3 and we call [getOffsetDistanceInPages] for page 1, the result would be -2,
     * meaning we're 2 pages away (moving backwards) to the current page.
     *
     * This offset also works in conjunction with [currentPageOffsetFraction], so if [currentPage]
     * is out of its snapped position (i.e. currentPageOffsetFraction!=0) then the calculated value
     * will still represent the offset in number of pages (in this case, not whole pages).
     * For instance, if currentPage=1 and we're slightly offset, currentPageOffsetFraction=0.2,
     * if we call this to page 2, the result would be 0.8, that is 0.8 page away from current page
     * (moving forward).
     *
     * @param page The page to calculate the offset from. This should be between 0 and [pageCount].
     * @return The offset of [page] with respect to [currentPage].
     */
    fun getOffsetDistanceInPages(page: Int): Float {
        require(page in 0..pageCount) {
            "page $page is not within the range 0 to $pageCount"
        }
        return page - currentPage - currentPageOffsetFraction
    }

    /**
     * When the user provided custom keys for the pages we can try to detect when there were
     * pages added or removed before our current page and keep this page as the current one
     * given that its index has been changed.
     */
    internal fun matchScrollPositionWithKey(
        itemProvider: PagerLazyLayoutItemProvider,
        currentPage: Int = Snapshot.withoutReadObservation { scrollPosition.currentPage }
    ): Int = scrollPosition.matchPageWithKey(itemProvider, currentPage)
}

internal suspend fun PagerState.animateToNextPage() {
    if (currentPage + 1 < pageCount) animateScrollToPage(currentPage + 1)
}

internal suspend fun PagerState.animateToPreviousPage() {
    if (currentPage - 1 >= 0) animateScrollToPage(currentPage - 1)
}

internal val DefaultPositionThreshold = 56.dp
private const val MaxPagesForAnimateScroll = 3
internal const val PagesToPrefetch = 1

internal val EmptyLayoutInfo = PagerMeasureResult(
    visiblePagesInfo = emptyList(),
    pageSize = 0,
    pageSpacing = 0,
    afterContentPadding = 0,
    orientation = Orientation.Horizontal,
    viewportStartOffset = 0,
    viewportEndOffset = 0,
    reverseLayout = false,
    beyondViewportPageCount = 0,
    positionedPages = emptyList(),
    firstVisiblePage = null,
    firstVisiblePageScrollOffset = 0,
    currentPage = null,
    currentPageOffsetFraction = 0.0f,
    canScrollForward = false,
    snapPosition = SnapPosition.Start,
    measureResult = object : MeasureResult {
        override val width: Int = 0

        override val height: Int = 0

        @Suppress("PrimitiveInCollection")
        override val alignmentLines: Map<AlignmentLine, Int> = mapOf()

        override fun placeChildren() {}
    },
    remeasureNeeded = false,
    coroutineScope = CoroutineScope(EmptyCoroutineContext)
)

private val UnitDensity = object : Density {
    override val density: Float = 1f
    override val fontScale: Float = 1f
}

private inline fun debugLog(generateMsg: () -> String) {
    if (PagerDebugConfig.PagerState) {
        println("PagerState: ${generateMsg()}")
    }
}

internal fun PagerLayoutInfo.calculateNewMaxScrollOffset(pageCount: Int): Long {
    val pageSizeWithSpacing = pageSpacing + pageSize
    val maxScrollPossible =
        (pageCount.toLong()) * pageSizeWithSpacing + beforeContentPadding + afterContentPadding
    val layoutSize =
        if (orientation == Orientation.Horizontal) viewportSize.width else viewportSize.height

    /**
     * We need to take into consideration the snap position for max scroll position.
     * For instance, if SnapPosition.Start, the max scroll position is
     * pageCount * pageSize - viewport. Now if SnapPosition.End, it should be pageCount * pageSize.
     * Therefore, the snap position discount varies between 0 and viewport.
     */
    val snapPositionDiscount = layoutSize - (snapPosition.position(
        layoutSize = layoutSize,
        itemSize = pageSize,
        itemIndex = pageCount - 1,
        beforeContentPadding = beforeContentPadding,
        afterContentPadding = afterContentPadding,
        itemCount = pageCount
    )).coerceIn(0, layoutSize)

    debugLog {
        "maxScrollPossible=$maxScrollPossible" +
            "\nsnapPositionDiscount=$snapPositionDiscount" +
            "\nlayoutSize=$layoutSize"
    }
    return (maxScrollPossible - snapPositionDiscount).coerceAtLeast(0L)
}

private fun PagerMeasureResult.calculateNewMinScrollOffset(pageCount: Int): Long {
    val layoutSize =
        if (orientation == Orientation.Horizontal) viewportSize.width else viewportSize.height

    return snapPosition.position(
        layoutSize = layoutSize,
        itemSize = pageSize,
        itemIndex = 0,
        beforeContentPadding = beforeContentPadding,
        afterContentPadding = afterContentPadding,
        itemCount = pageCount
    ).coerceIn(0, layoutSize).toLong()
}

//@OptIn(ExperimentalFoundationApi::class)
//private suspend fun LazyLayoutAnimateScrollScope.animateScrollToPage(
//    targetPage: Int,
//    targetPageOffsetToSnappedPosition: Float,
//    animationSpec: AnimationSpec<Float>,
//    updateTargetPage: ScrollScope.(Int) -> Unit
//) {
//    scroll {
//        updateTargetPage(targetPage)
//        val forward = targetPage > firstVisibleItemIndex
//        val visiblePages = lastVisibleItemIndex - firstVisibleItemIndex + 1
//        if (((forward && targetPage > lastVisibleItemIndex) ||
//                (!forward && targetPage < firstVisibleItemIndex)) &&
//            abs(targetPage - firstVisibleItemIndex) >= MaxPagesForAnimateScroll
//        ) {
//            val preJumpPosition = if (forward) {
//                (targetPage - visiblePages).coerceAtLeast(firstVisibleItemIndex)
//            } else {
//                (targetPage + visiblePages).coerceAtMost(firstVisibleItemIndex)
//            }
//
//            debugLog {
//                "animateScrollToPage with pre-jump to position=$preJumpPosition"
//            }
//
//            // Pre-jump to 1 viewport away from destination page, if possible
//            snapToItem(preJumpPosition, 0)
//        }
//
//        // The final delta displacement will be the difference between the pages offsets
//        // discounting whatever offset the original page had scrolled plus the offset
//        // fraction requested by the user.
//        val displacement = calculateDistanceTo(targetPage) + targetPageOffsetToSnappedPosition
//
//        debugLog { "animateScrollToPage $displacement pixels" }
//        var previousValue = 0f
//        animate(0f, displacement, animationSpec = animationSpec) { currentValue, _ ->
//            val delta = currentValue - previousValue
//            val consumed = scrollBy(delta)
//            debugLog { "Dispatched Delta=$delta Consumed=$consumed" }
//            previousValue += consumed
//        }
//    }
//}
